package com.nikmesoft.android.ebox.views;

import java.util.ArrayList;
import java.util.List;
import com.nikmesoft.android.ebox.R;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Debug;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;

public class LockPatternView extends View {
	// constants
	private final static int PATH_COLOR = Color.WHITE;
	private final static int PATH_ALPHA = 100;
	private final static int PADDING_TOP = 0;
	private final static int PADDING_BOTTOM = 0;
	private final static int PADDING_LEFT = 0;
	private final static int PADDING_RIGHT = 0;
	private final static float HIT_FACTOR = 0.6f;
	private final static float DIAMETER_FACTOR = 0.10f;
	private Paint mPathPaint;
	private Paint mCellPaint;
	private Context mContext;
	private Bitmap mBitmapPointDefault;
	private Bitmap mBitmapPointTouched;
	private Bitmap mBitmapSelectedDefault;
	private Bitmap mBitmapSelectedMatch;
	private Bitmap mBitmapSelectedNotMatch;
	private Bitmap mBitmapArrowGreenUp;
	private Bitmap mBitmapArrowRedUp;
	private DisplayMode mPatternDisplayMode;
	private float mCellWidth;
	private float mCellHeight;
	private float mMaxBitmapWidth;
	private float mMaxBitmapHeight;
	private Matrix mBitmapSelectedMatrix;
	private ArrayList<Cell> mPattern;
	private boolean[][] mPatternTracking;;
	private OnPatternListener mOnPatternListener;
	private boolean mPatternInProgress = false;
	private float mInProgressX = -1;
	private float mInProgressY = -1;
	private static final boolean PROFILE_DRAWING = false;
	private boolean mDrawingProfilingStarted = false;
	private boolean mInStealthMode = false;
	private final Path mCurrentPath = new Path();
	private final Rect mInvalidate = new Rect();
	private final Matrix mArrowMatrix = new Matrix();

	public LockPatternView(Context context, AttributeSet attrs) {
		super(context, attrs);
		setClickable(true);
		mContext = context;
		mPatternDisplayMode = DisplayMode.Match;
		mBitmapSelectedMatrix = new Matrix();
		mPattern = new ArrayList<Cell>(9);
		mPatternTracking = new boolean[3][3];
		// init cellPaint
		mCellPaint = new Paint();
		// init pathPaint
		mPathPaint = new Paint();
		mPathPaint.setAntiAlias(true);
		mPathPaint.setDither(true);
		mPathPaint.setColor(PATH_COLOR);
		mPathPaint.setAlpha(PATH_ALPHA);
		mPathPaint.setStyle(Paint.Style.STROKE);
		mPathPaint.setStrokeJoin(Paint.Join.ROUND);
		mPathPaint.setStrokeCap(Paint.Cap.ROUND);

		// init bitmaps
		mBitmapPointDefault = getBitmapFromResID(R.drawable.ic_lock_point_default);
		mBitmapPointTouched = getBitmapFromResID(R.drawable.ic_lock_point_touched);
		mBitmapSelectedDefault = getBitmapFromResID(R.drawable.ic_lock_selected_default);
		mBitmapSelectedMatch = getBitmapFromResID(R.drawable.ic_lock_selected_match);
		mBitmapSelectedNotMatch = getBitmapFromResID(R.drawable.ic_lock_selected_notmatch);

		mBitmapArrowGreenUp = getBitmapFromResID(R.drawable.ic_lock_drag_direction_green_up);
		mBitmapArrowRedUp = getBitmapFromResID(R.drawable.ic_lock_drag_direction_red_up);

		// bitmaps have the size of the largest bitmap in this group
		final Bitmap bitmaps[] = { mBitmapPointDefault, mBitmapPointTouched,
				mBitmapSelectedDefault, mBitmapSelectedMatch,
				mBitmapSelectedNotMatch };

		for (Bitmap bitmap : bitmaps) {
			mMaxBitmapWidth = Math.max(mMaxBitmapWidth, bitmap.getWidth());
			mMaxBitmapHeight = Math.max(mMaxBitmapHeight, bitmap.getHeight());
		}
	}

	private Bitmap getBitmapFromResID(int resId) {
		return BitmapFactory.decodeResource(getContext().getResources(), resId);
	}

	public void setPattern(DisplayMode displayMode, List<Cell> patterns) {
		mPattern.clear();
		mPattern.addAll(patterns);
		clearPatternTracking();
		for (Cell cell : patterns) {
			mPatternTracking[cell.getRow()][cell.getColumn()] = true;
		}

		setDisplayMode(displayMode);
	}

	private void sendAccessEvent(int resId) {
		setContentDescription(mContext.getString(resId));
		sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
		setContentDescription(null);
	}

	private void notifyCellAdded() {
		if (mOnPatternListener != null) {
			mOnPatternListener.onPatternCellAdded(mPattern);
		}
		sendAccessEvent(R.string.lockpattern_cell_added);
	}

	private void notifyPatternStarted() {
		if (mOnPatternListener != null) {
			mOnPatternListener.onPatternStart();
		}
		sendAccessEvent(R.string.lockpattern_pattern_started);
	}

	private void notifyPatternDetected() {
		if (mOnPatternListener != null) {
			mOnPatternListener.onPatternDetected(mPattern);
		}
		sendAccessEvent(R.string.lockpattern_pattern_detected);
	}

	private void notifyPatternCleared() {
		if (mOnPatternListener != null) {
			mOnPatternListener.onPatternCleared();
		}
		sendAccessEvent(R.string.lockpattern_pattern_cleared);
	}

	public void clearPattern() {
		resetPattern();
	}

	private void resetPattern() {
		mPattern.clear();
		clearPatternTracking();
		mPatternDisplayMode = DisplayMode.Match;
		invalidate();
	}

	private void clearPatternTracking() {
		for (int i = 0; i < 3; i++) {
			for (int j = 0; j < 3; j++) {
				mPatternTracking[i][j] = false;
			}
		}
	}

	public void setDisplayMode(DisplayMode displayMode) {
		mPatternDisplayMode = displayMode;
		if (displayMode == DisplayMode.Animation) {
			if (mPattern.size() == 0) {
				throw new IllegalStateException(
						"you must have a pattern to "
								+ "animate if you want to set the display mode to animate");
			}
			//
		}
		invalidate();
	}

	public DisplayMode getDisplayMode() {
		return mPatternDisplayMode;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		final ArrayList<Cell> pattern = mPattern;
		final int count = pattern.size();
		final boolean[][] drawLookup = mPatternTracking;

		float radius = (mCellWidth * DIAMETER_FACTOR * 0.5f);
		mPathPaint.setStrokeWidth(radius);

		final Path currentPath = mCurrentPath;
		currentPath.rewind();
		// draw the cells
		for (int i = 0; i < 3; i++) {
			float topY = PADDING_TOP + i * mCellHeight;
			for (int j = 0; j < 3; j++) {
				float leftX = PADDING_LEFT + j * mCellWidth;
				drawCell(canvas, (int) leftX, (int) topY, drawLookup[i][j]);
			}
		}

		final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.NotMatch);

		// draw the arrows associated with the path (unless the user is in
		// progress, and
		// we are in stealth mode)
		boolean oldFlag = (mCellPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0;
		mCellPaint.setFilterBitmap(true); // draw with higher quality since we
											// render with transforms
		if (drawPath) {
			for (int i = 0; i < count - 1; i++) {
				Cell cell = pattern.get(i);
				Cell next = pattern.get(i + 1);

				// only draw the part of the pattern stored in
				// the lookup table (this is only different in the case
				// of animation).
				if (!drawLookup[next.row][next.column]) {
					break;
				}

				float leftX = PADDING_LEFT + cell.column * mCellWidth;
				float topY = PADDING_TOP + cell.row * mCellHeight;

				drawArrow(canvas, leftX, topY, cell, next);
			}
		}

		if (drawPath) {
			boolean anyCircles = false;
			for (int i = 0; i < count; i++) {
				Cell cell = pattern.get(i);

				// only draw the part of the pattern stored in
				// the lookup table (this is only different in the case
				// of animation).
				if (!drawLookup[cell.row][cell.column]) {
					break;
				}
				anyCircles = true;

				float centerX = getCenterXForColumn(cell.column);
				float centerY = getCenterYForRow(cell.row);
				if (i == 0) {
					currentPath.moveTo(centerX, centerY);
				} else {
					currentPath.lineTo(centerX, centerY);
				}
			}

			// add last in progress section
			if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animation)
					&& anyCircles) {
				currentPath.lineTo(mInProgressX, mInProgressY);
			}
			canvas.drawPath(currentPath, mPathPaint);
		}

		mCellPaint.setFilterBitmap(oldFlag); // restore default flag
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		final int width = w - PADDING_LEFT - PADDING_RIGHT;
		mCellWidth = width / 3.0f;

		final int height = h - PADDING_TOP - PADDING_BOTTOM;
		mCellHeight = height / 3.0f;
	}

	private void drawArrow(Canvas canvas, float leftX, float topY, Cell start,
			Cell end) {
		boolean green = mPatternDisplayMode != DisplayMode.NotMatch;

		final int endRow = end.row;
		final int startRow = start.row;
		final int endColumn = end.column;
		final int startColumn = start.column;

		// offsets for centering the bitmap in the cell
		final int offsetX = (int) (mCellWidth - mMaxBitmapWidth) / 2;
		final int offsetY = (int) (mCellHeight - mMaxBitmapHeight) / 2;

		// compute transform to place arrow bitmaps at correct angle inside
		// circle.
		// This assumes that the arrow image is drawn at 12:00 with it's top
		// edge
		// coincident with the circle bitmap's top edge.
		Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp;
		final int cellWidth = (int) mMaxBitmapWidth;
		final int cellHeight = (int) mMaxBitmapHeight;

		// the up arrow bitmap is at 12:00, so find the rotation from x axis and
		// add 90 degrees.
		final float theta = (float) Math.atan2((double) (endRow - startRow),
				(double) (endColumn - startColumn));
		final float angle = (float) Math.toDegrees(theta) + 90.0f;

		// compose matrix
		float sx = Math.min(mCellWidth / mMaxBitmapWidth, 1.0f);
		float sy = Math.min(mCellHeight / mMaxBitmapHeight, 1.0f);
		mArrowMatrix.setTranslate(leftX + offsetX, topY + offsetY); // transform
																	// to cell
																	// position
		mArrowMatrix.preTranslate(mMaxBitmapWidth / 2, mMaxBitmapHeight / 2);
		mArrowMatrix.preScale(sx, sy);
		mArrowMatrix.preTranslate(-mMaxBitmapWidth / 2, -mMaxBitmapHeight / 2);
		mArrowMatrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate
																			// about
																			// cell
																			// center
		mArrowMatrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate
																				// to
																				// 12:00
																				// pos
		canvas.drawBitmap(arrow, mArrowMatrix, mCellPaint);
	}

	private void drawCell(Canvas canvas, int left, int top,
			boolean isSelectedPattern) {
		Bitmap mBitmapPoint;
		Bitmap mBitmapSelected;

		if (!isSelectedPattern
				|| (mInStealthMode && mPatternDisplayMode != DisplayMode.NotMatch)) {
			// unselected circle
			mBitmapPoint = mBitmapPointDefault;
			mBitmapSelected = mBitmapSelectedDefault;
		} else if (mPatternInProgress) {
			mBitmapSelected = mBitmapSelectedMatch;
			mBitmapPoint = mBitmapPointTouched;
		} else if (mPatternDisplayMode == DisplayMode.NotMatch) {
			// the pattern is wrong
			mBitmapSelected = mBitmapSelectedNotMatch;
			mBitmapPoint = mBitmapPointDefault;
		} else if (mPatternDisplayMode == DisplayMode.Match
				|| mPatternDisplayMode == DisplayMode.Animation) {
			// the pattern is correct
			mBitmapSelected = mBitmapSelectedMatch;
			mBitmapPoint = mBitmapPointDefault;
		} else {
			throw new IllegalStateException("Unknown display mode "
					+ mPatternDisplayMode);
		}

		int offsetX = (int) ((mCellWidth - mMaxBitmapWidth) / 2f);
		int offsetY = (int) ((mCellHeight - mMaxBitmapHeight) / 2f);

		// Allow circles to shrink if the view is too small to hold them.
		float sx = Math.min(mCellWidth / mMaxBitmapWidth, 1.0f);
		float sy = Math.min(mCellHeight / mMaxBitmapHeight, 1.0f);

		mBitmapSelectedMatrix.setTranslate(left + offsetX, top + offsetY);
		mBitmapSelectedMatrix.preTranslate(mMaxBitmapWidth / 2,
				mMaxBitmapHeight / 2);
		mBitmapSelectedMatrix.preScale(sx, sy);
		mBitmapSelectedMatrix.preTranslate(-mMaxBitmapWidth / 2,
				-mMaxBitmapHeight / 2);

		canvas.drawBitmap(mBitmapPoint, mBitmapSelectedMatrix, mCellPaint);
		canvas.drawBitmap(mBitmapSelected, mBitmapSelectedMatrix, mCellPaint);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			handleActionDown(event);
			return true;
		case MotionEvent.ACTION_UP:
			handleActionUp(event);
			return true;
		case MotionEvent.ACTION_MOVE:
			handleActionMove(event);
			return true;
		case MotionEvent.ACTION_CANCEL:
			resetPattern();
			mPatternInProgress = false;
			notifyPatternCleared();
			if (PROFILE_DRAWING) {
				if (mDrawingProfilingStarted) {
					Debug.stopMethodTracing();
					mDrawingProfilingStarted = false;
				}
			}
			return true;
		}
		return false;
	}

	// classes
	public enum DisplayMode {
		Match, Animation, NotMatch
	}

	public static class Cell {

		int row;
		int column;
		static Cell[][] cells = new Cell[3][3];
		static {
			for (int i = 0; i < 3; i++) {
				for (int j = 0; j < 3; j++) {
					cells[i][j] = new Cell(i, j);
				}
			}
		}

		private Cell(int row, int column) {
			checkRange(row, column);
			this.row = row;
			this.column = column;
		}

		public int getRow() {
			return row;
		}

		public int getColumn() {
			return column;
		}

		public static synchronized Cell of(int row, int column) {
			checkRange(row, column);
			return cells[row][column];
		}

		private static void checkRange(int row, int column) {
			if (row < 0 || row > 2) {
				throw new IllegalArgumentException("row must be in range 0-2");
			}
			if (column < 0 || column > 2) {
				throw new IllegalArgumentException(
						"column must be in range 0-2");
			}
		}

		public String toString() {
			return "(row=" + row + ",clmn=" + column + ")";
		}
	}

	public static interface OnPatternListener {

		void onPatternStart();

		void onPatternCleared();

		void onPatternCellAdded(List<Cell> pattern);

		void onPatternDetected(List<Cell> pattern);
	}

	public void setOnPatternListener(OnPatternListener onPatternListener) {
		mOnPatternListener = onPatternListener;
	}

	private void handleActionDown(MotionEvent event) {
		resetPattern();
		final float x = event.getX();
		final float y = event.getY();
		final Cell hitCell = detectAndAddHit(x, y);
		if (hitCell != null) {
			mPatternInProgress = true;
			mPatternDisplayMode = DisplayMode.Match;
			notifyPatternStarted();
		} else {
			mPatternInProgress = false;
			notifyPatternCleared();
		}
		if (hitCell != null) {
			final float startX = getCenterXForColumn(hitCell.column);
			final float startY = getCenterYForRow(hitCell.row);

			final float widthOffset = mCellWidth / 2f;
			final float heightOffset = mCellHeight / 2f;

			invalidate((int) (startX - widthOffset),
					(int) (startY - heightOffset),
					(int) (startX + widthOffset), (int) (startY + heightOffset));
		}
		mInProgressX = x;
		mInProgressY = y;
		if (PROFILE_DRAWING) {
			if (!mDrawingProfilingStarted) {
				Debug.startMethodTracing("LockPatternDrawing");
				mDrawingProfilingStarted = true;
			}
		}
	}

	private void handleActionUp(MotionEvent event) {
		// report pattern detected
		if (!mPattern.isEmpty()) {
			mPatternInProgress = false;
			notifyPatternDetected();
			invalidate();
		}
		if (PROFILE_DRAWING) {
			if (mDrawingProfilingStarted) {
				Debug.stopMethodTracing();
				mDrawingProfilingStarted = false;
			}
		}
	}

	private void handleActionMove(MotionEvent event) {
		// Handle all recent motion events so we don't skip any cells even when
		// the device
		// is busy...
		final int historySize = event.getHistorySize();
		for (int i = 0; i < historySize + 1; i++) {
			final float x = i < historySize ? event.getHistoricalX(i) : event
					.getX();
			final float y = i < historySize ? event.getHistoricalY(i) : event
					.getY();
			final int patternSizePreHitDetect = mPattern.size();
			Cell hitCell = detectAndAddHit(x, y);
			final int patternSize = mPattern.size();
			if (hitCell != null && patternSize == 1) {
				mPatternInProgress = true;
				notifyPatternStarted();
			}
			// note current x and y for rubber banding of in progress patterns
			final float dx = Math.abs(x - mInProgressX);
			final float dy = Math.abs(y - mInProgressY);
			if (dx + dy > mCellWidth * 0.01f) {
				float oldX = mInProgressX;
				float oldY = mInProgressY;

				mInProgressX = x;
				mInProgressY = y;

				if (mPatternInProgress && patternSize > 0) {
					final ArrayList<Cell> pattern = mPattern;
					final float radius = mCellWidth * DIAMETER_FACTOR * 0.5f;

					final Cell lastCell = pattern.get(patternSize - 1);

					float startX = getCenterXForColumn(lastCell.column);
					float startY = getCenterYForRow(lastCell.row);

					float left;
					float top;
					float right;
					float bottom;

					final Rect invalidateRect = mInvalidate;

					if (startX < x) {
						left = startX;
						right = x;
					} else {
						left = x;
						right = startX;
					}

					if (startY < y) {
						top = startY;
						bottom = y;
					} else {
						top = y;
						bottom = startY;
					}

					// Invalidate between the pattern's last cell and the
					// current location
					invalidateRect.set((int) (left - radius),
							(int) (top - radius), (int) (right + radius),
							(int) (bottom + radius));

					if (startX < oldX) {
						left = startX;
						right = oldX;
					} else {
						left = oldX;
						right = startX;
					}

					if (startY < oldY) {
						top = startY;
						bottom = oldY;
					} else {
						top = oldY;
						bottom = startY;
					}

					// Invalidate between the pattern's last cell and the
					// previous location
					invalidateRect.union((int) (left - radius),
							(int) (top - radius), (int) (right + radius),
							(int) (bottom + radius));

					// Invalidate between the pattern's new cell and the
					// pattern's previous cell
					if (hitCell != null) {
						startX = getCenterXForColumn(hitCell.column);
						startY = getCenterYForRow(hitCell.row);

						if (patternSize >= 2) {
							// (re-using hitcell for old cell)
							hitCell = pattern.get(patternSize - 1
									- (patternSize - patternSizePreHitDetect));
							oldX = getCenterXForColumn(hitCell.column);
							oldY = getCenterYForRow(hitCell.row);

							if (startX < oldX) {
								left = startX;
								right = oldX;
							} else {
								left = oldX;
								right = startX;
							}

							if (startY < oldY) {
								top = startY;
								bottom = oldY;
							} else {
								top = oldY;
								bottom = startY;
							}
						} else {
							left = right = startX;
							top = bottom = startY;
						}

						final float widthOffset = mCellWidth / 2f;
						final float heightOffset = mCellHeight / 2f;

						invalidateRect.set((int) (left - widthOffset),
								(int) (top - heightOffset),
								(int) (right + widthOffset),
								(int) (bottom + heightOffset));
					}

					invalidate(invalidateRect);
				} else {
					invalidate();
				}
			}
		}
	}

	private float getCenterXForColumn(int column) {
		return PADDING_LEFT + column * mCellWidth + mCellWidth / 2f;
	}

	private float getCenterYForRow(int row) {
		return PADDING_TOP + row * mCellHeight + mCellHeight / 2f;
	}

	private int getRowHit(float y) {

		float hitSize = mCellHeight * HIT_FACTOR;

		float offset = PADDING_TOP + (mCellHeight - hitSize) / 2f;
		for (int i = 0; i < 3; i++) {

			final float hitTop = offset + mCellHeight * i;
			if (y >= hitTop && y <= hitTop + hitSize) {
				return i;
			}
		}
		return -1;
	}

	private int getColumnHit(float x) {
		float hitSize = mCellWidth * HIT_FACTOR;

		float offset = PADDING_LEFT + (mCellWidth - hitSize) / 2f;
		for (int i = 0; i < 3; i++) {

			final float hitLeft = offset + mCellWidth * i;
			if (x >= hitLeft && x <= hitLeft + hitSize) {
				return i;
			}
		}
		return -1;
	}

	private Cell checkForNewHit(float x, float y) {

		final int rowHit = getRowHit(y);
		if (rowHit < 0) {
			return null;
		}
		final int columnHit = getColumnHit(x);
		if (columnHit < 0) {
			return null;
		}

		if (mPatternTracking[rowHit][columnHit]) {
			return null;
		}
		return Cell.of(rowHit, columnHit);
	}

	private Cell detectAndAddHit(float x, float y) {
		final Cell cell = checkForNewHit(x, y);
		if (cell != null) {

			// check for gaps in existing pattern
			Cell fillInGapCell = null;
			final ArrayList<Cell> pattern = mPattern;
			if (!pattern.isEmpty()) {
				final Cell lastCell = pattern.get(pattern.size() - 1);
				int dRow = cell.row - lastCell.row;
				int dColumn = cell.column - lastCell.column;

				int fillInRow = lastCell.row;
				int fillInColumn = lastCell.column;

				if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
					fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
				}

				if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
					fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
				}

				fillInGapCell = Cell.of(fillInRow, fillInColumn);
			}

			if (fillInGapCell != null
					&& !mPatternTracking[fillInGapCell.row][fillInGapCell.column]) {
				addCellToPattern(fillInGapCell);
			}
			addCellToPattern(cell);
			return cell;
		}
		return null;
	}

	private void addCellToPattern(Cell newCell) {
		mPatternTracking[newCell.getRow()][newCell.getColumn()] = true;
		mPattern.add(newCell);
		notifyCellAdded();
	}

	@SuppressWarnings("unchecked")
	public List<Cell> getPattern() {
		return (List<Cell>) mPattern.clone();
	}

}
